package org.awa.demo.opencv.task.kunming;

import lombok.Builder;
import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;

@Slf4j
public class BilinearInterpolator {

    private int size;//扩展倍率
    private int sizeRate;//数组和实际地理位置的映射
    private int halfSize;//一半的扩展倍率

    private Double[][] dataList;//原始数组
    private Double[][] resultList;

    private int widthSize;//扩展后的x尺寸
    private int heightSize;//扩展后的y尺寸

    private int widthMaxRange;//冗余数据，矩阵边缘要切换成IDW算法，这里存切换算法的临界index
    private int heightMaxRange;//冗余数据，矩阵边缘要切换成IDW算法，这里存切换算法的临界index

    private int _x;
    private int _y;

    @Builder
    public BilinearInterpolator(int size, int sideLen,Double[][] dataList) {
        //放大倍率必须单数，不是单数的加1处理
        if(size % 2 != 1){
            size = size + 1;
        }
        this.size = size;
        this.sizeRate = sideLen * size;
        this.halfSize = (size + 1) / 2;
        this.dataList = dataList;

        this.widthSize = dataList[0].length * size;
        this.heightSize = dataList.length * size;
        this.resultList = new Double[heightSize][widthSize];

        this.heightMaxRange = heightSize - halfSize;
        this.widthMaxRange = widthSize - halfSize;
    }

    /*
                因为原始点的位置为 x = n * size + halfSize，所以mod == halfSize时，正好是原始点。而不等于这个的，就是扩展点。当x< halfSize 或x > totalSize-halfSize的，就是边缘点。
                还不理解的可以画个点阵帮助理解。

                e.g: size=3，value(yx),其中[]代表原始点，边上一圈就是边缘点，其他的是扩展点。而边缘点可以分为四个角和4条边8个区域，角取一个原始点，边取两个原始点做计算
                11   12   13 | 14   15   16 | 17   18   19

                21  [22]  23 | 24  [25]  26 | 27  [28]  29

                31   32   33 | 34   35   36 | 37   38   39
                ------------------------------------------
                41   42   43 | 44   45   46 | 47   48   49

                51  [52]  53 | 54  [55]  56 | 57  [58]  59

                61   62   63 | 64   65   66 | 67   68   69
                ------------------------------------------
                71   72   73 | 74   75   76 | 77   78   79

                81  [82]  83 | 84  [85]  86 | 87  [88]  89

                91   92   93 | 94   95   96 | 97   98   99
                */
    public Double[][] interpolate() {
        try{
            //数组坐标是从0开始，矩阵换算坐标是从1开始。为方便直观理解，计算时统一使用矩阵换算坐标，取值赋值使用数组坐标。用专门的取值赋值函数封装转换逻辑。
            for (int x = 1; x <= widthSize; x++) {
                _x = x;
                for (int y = 1; y <= heightSize; y++) {
                    _y = y;

                    int modX = x % size;
                    int mody = y % size;

                    if(modX == halfSize && mody == halfSize) {
                        //原始点，通过映射关系到原始数据集里拿数据直接赋值
                        //矩阵假想是横向x，竖向y，与数组结构是反的，数组结构表示为 int[y][x]
                        setResultValueByLocation(x,y, getValueFromDataList(getDataListLocation(x, y)));
                        continue;
                    }

                    if((x < halfSize || x > widthMaxRange) || (y < halfSize || y > heightMaxRange)){
                        //边缘点，分8个区域拿1至2个点的数据，做IDW插值
                        int[] distLocation = new int[]{x, y};
                        List<int[]> pointLocationList = new ArrayList<>();
                        List<Double> value = new ArrayList<>();

                        if((x < halfSize || x > widthMaxRange) && (y < halfSize || y > heightMaxRange)){
                            //4个角，1个点
                            if(x < halfSize && y < halfSize){
                                //左上角
                                addIdwPoint(halfSize,halfSize,pointLocationList,value);
                            }else if(x < halfSize && y > heightMaxRange){
                                //左下角
                                addIdwPoint(halfSize,heightMaxRange,pointLocationList,value);
                            }else if(x > widthMaxRange && y < halfSize){
                                //右上角
                                addIdwPoint(widthMaxRange,halfSize,pointLocationList,value);
                            }else{
                                //右下角
                                addIdwPoint(widthMaxRange,heightMaxRange,pointLocationList,value);
                            }
                        }else{
                            //4条边，两个点
                            if(x < halfSize){
                                //左边
                                int[] yRange = getRange(y,mody);
                                addIdwPoint(halfSize,yRange[0],pointLocationList,value);
                                addIdwPoint(halfSize,yRange[1],pointLocationList,value);
                            }else if(x > widthMaxRange){
                                //右边
                                int[] yRange = getRange(y,mody);
                                addIdwPoint(widthMaxRange,yRange[0],pointLocationList,value);
                                addIdwPoint(widthMaxRange,yRange[1],pointLocationList,value);
                            }else if(y < halfSize){
                                //上边
                                int[] xRange = getRange(x,modX);
                                addIdwPoint(xRange[0],halfSize,pointLocationList,value);
                                addIdwPoint(xRange[1],halfSize,pointLocationList,value);
                            }else{
                                //下边
                                int[] xRange = getRange(x,modX);
                                addIdwPoint(xRange[0],heightMaxRange,pointLocationList,value);
                                addIdwPoint(xRange[1],heightMaxRange,pointLocationList,value);
                            }
                        }
                        setResultValueByLocation(x,y, interpolateIdw(distLocation,pointLocationList,value));
                        continue;
                    }

                    //扩展点，拿前后左右最近的4个原始值做双线性插值

                    //获取result数组前后左右四个点原始值的坐标
                    int[] xRange = getRange(x,modX);
                    int[] yRange = getRange(y,mody);
                    //获取前后左右四个点原始值对应的dataList坐标
                    int[] minLocation = getDataListLocation(xRange[0],yRange[0]);
                    int[] maxLocation = getDataListLocation(xRange[1],yRange[1]);
                    //双线性插值

                    Double value = interpolateBilinear(x, y, xRange[0], xRange[1], yRange[0], yRange[1],
                            getValueFromDataList(minLocation[0],minLocation[1]),//value11
                            getValueFromDataList(minLocation[0],maxLocation[1]),//value12
                            getValueFromDataList(maxLocation[0],minLocation[1]),//value21
                            getValueFromDataList(maxLocation[0],maxLocation[1]));//value22
                    setResultValueByLocation(x, y,value);
                }
            }
        }catch (Exception e){
            e.printStackTrace();
            log.error(">> exception status: resultList[{}][{}],dateList[{}][{}],currIndex:{},{},halfSize:{},size:{},heightMaxRange:{},widthMaxRange:{}",
                    heightSize,widthSize,dataList.length,dataList[0].length,_x,_y,halfSize,size,heightMaxRange,widthMaxRange);
            log.error("bilinear exception: "+e.toString(),e);
            throw e;
        }
        return resultList;
    }

    //根据index添加对应原始点数据
    private void addIdwPoint(int x, int y, List<int[]> pointLocationList,List<Double> value){
        pointLocationList.add(new int[]{ x, y });//pointLocationList是给IDW算法计算插值用的，所以用的是矩阵坐标，不用数组坐标
        value.add(getValueFromDataList(getDataListLocation(x, y)));//dataListLocation已经转成了数组坐标，所以直接去数组取值就好
    }

    //获取index所处的前后原始值起始index
    private int[] getRange(int index,int mod){
        int value1,value2;
        if(mod < halfSize){
            //y轴方向上更靠近下侧的点，所以先定位y2，再定位y1
            value2 = index + halfSize - mod;
            value1 = value2 - size;
        }else{
            //y轴方向上更靠近上侧的点，所以先定位y1，再定位y2
            value1 = index - (mod  - halfSize);
            value2 = index + size;
        }
        return new int[]{value1,value2};
    }

    //返回值为[x,y]。
    private int[] getDataListLocation(int x, int y){
        int[] result = new int[2];
        result[0] = 1 + (x - halfSize) / size;
        result[1] = 1 + (y - halfSize) / size;
        return result;
    }

    //共原始dataList数据集中取值。数组是0开头的，数学矩阵是1开头的，所以遍历计算时使用1开头的算出数值，而从数组中取值赋值需要使用0开头的，中间得减1。
    //矩阵假想是横向x，竖向y，与数组结构是反的，数组结构表示为 int[y][x]
    private Double getValueFromDataList(int x,int y){
        return dataList[y - 1][x - 1];
    }

    private Double getValueFromDataList(int[] dataLocation){
        return getValueFromDataList(dataLocation[0], dataLocation[1]);
    }

    //赋值到resultList数据集中。数组是0开头的，数学矩阵是1开头的，所以遍历计算时使用1开头的算出数值，而从数组中取值赋值需要使用0开头的，中间得减1
    //矩阵假想是横向x，竖向y，与数组结构是反的，数组结构表示为 int[y][x]
    private void setResultValueByLocation(int x,int y,Double value){
        this.resultList[y - 1][x - 1] = value;
    }

    //双线性插值
    private Double interpolateBilinear(int x,int y,int x1,int x2,int y1,int y2,double value11,double value12,double value21,double value22){
        double total = value11 * (x2 - x) * (y2 - y) + value21 * (x - x1) * (y2 - y) + value12 * (x2 - x) * (y - y1) + value22 * (x - x1) * (y - y1);
        double div = (x2 - x1) * (y2 - y1);
        return total/div;
    }

    //反距离加权插值（IDW）
    private Double interpolateIdw(int[] distLocation, List<int[]> pointLocationList,List<Double> value){
        double[] wArr = new double[pointLocationList.size()];
        double[] distanceArr = new double[pointLocationList.size()];
        for (int i = 0; i < wArr.length; i++) {
            Double distance = getDistance(distLocation, pointLocationList.get(i));
            distanceArr[i] = distance;
            wArr[i] = 1 / Math.pow(distance, 2);
        }

        double total = 0;
        double div = 0;
        for (int i = 0; i < wArr.length; i++) {
            total += wArr[i] * value.get(i);
            div += wArr[i];
        }
        return total/div;
    }

    private Double getDistance(int[] distLocation,int[] sourceLocation){
        double xRange = Math.abs(sourceLocation[0] - distLocation[0]);
        double yRange = Math.abs(sourceLocation[1] - distLocation[1]);
        return Math.sqrt(Math.pow(xRange,2) + Math.pow(yRange,2)) * sizeRate;
    }
}
